all: implement RFC8910 captive portal (CP) option for DHCPv4
authorPaul Donald <[email protected]>
Mon, 17 Nov 2025 19:43:15 +0000 (20:43 +0100)
committerÁlvaro Fernández Rojas <[email protected]>
Tue, 18 Nov 2025 10:53:02 +0000 (11:53 +0100)
https://www.rfc-editor.org/rfc/rfc8910.html

Signed-off-by: Paul Donald <[email protected]>
Link: https://github.com/openwrt/odhcpd/pull/315
Signed-off-by: Álvaro Fernández Rojas <[email protected]>
README.md
src/config.c
src/dhcpv4.c
src/dhcpv4.h

index 1ac77420d96c326eee7f62e0f8503c2379339e80..2d8c3f2e5817f6613495cc61080f7c1b956e1547 100644 (file)
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ and may also receive information from ubus
 | prefix_filter                |string |`::/0` | Only advertise on-link prefixes within the provided IPv6 prefix; others are filtered out. [IPv6 prefix] |
 | ntp                  |list   |`<local address>`| NTP servers to announce accepts IPv4 and IPv6 |
 | upstream             |list   | -     | A list of interfaces which can be used as a source of configuration information (e.g. for NTP servers, if not set explicitly). |
-| captive_portal_uri |string | no  | The API URI to be sent in RFC8910 captive portal options, via DHCPv6 and ICMPv6 RA. |
+| captive_portal_uri |string | no  | The API URI to be sent in RFC8910 captive portal options, via DHCPv4, DHCPv6, and ICMPv6 RA. |
 
 [//]: # "dhcpv6_raw - string - not documented, may change when generic DHCPv4/DHCPv6 options are added"
 
index b2991ef13994eb513ece281da4167138df76de51..865dadfe6b2d025060c0da7d2ee36f070c254dba 100644 (file)
@@ -1310,6 +1310,11 @@ int config_parse_interface(void *data, size_t len, const char *name, bool overwr
        if ((c = tb[IFACE_ATTR_CAPTIVE_PORTAL_URI])) {
                iface->captive_portal_uri = strdup(blobmsg_get_string(c));
                iface->captive_portal_uri_len = strlen(iface->captive_portal_uri);
+               if (iface->captive_portal_uri_len > UINT8_MAX) {
+                       warn("RFC8910 captive portal URI > %d characters for interface '%s': option via DHCPv4 not possible",
+                               UINT8_MAX,
+                               iface->name);
+               }
                debug("Set RFC8910 captive portal URI: '%s' for interface '%s'",
                        iface->captive_portal_uri, iface->name);
        }
index cf5abf5d61ef6844b1588e852b15f5f3dbd85f82..9ab78695ad3798ad1817cd1b954fd6f785adaf84 100644 (file)
@@ -779,6 +779,7 @@ enum {
        IOV_FR_NONCE_CAP,
        IOV_DNR,
        IOV_DNR_BODY,
+       IOV_CAPTIVE_PORTAL,
        IOV_END,
        IOV_PADDING,
        IOV_TOTAL
@@ -920,6 +921,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                [IOV_FR_NONCE_CAP]      = { &reply_fr_nonce_cap, 0 },
                [IOV_DNR]               = { &reply_dnr, 0 },
                [IOV_DNR_BODY]          = { NULL, 0 },
+               [IOV_CAPTIVE_PORTAL]    = { NULL, 0 },
                [IOV_END]               = { &reply_end, sizeof(reply_end) },
                [IOV_PADDING]           = { NULL, 0 },
        };
@@ -938,6 +940,7 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                DHCPV4_OPT_CLIENTID, // Must be in reply if present in req, RFC6842, §3
                DHCPV4_OPT_AUTHENTICATION,
                DHCPV4_OPT_SEARCH_DOMAIN,
+               DHCPV4_OPT_CAPTIVE_PORTAL,
                DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE,
        };
 
@@ -1296,6 +1299,22 @@ void dhcpv4_handle_msg(void *src_addr, void *data, size_t len,
                        iov[IOV_DNR_BODY].iov_base = dnrs;
                        iov[IOV_DNR_BODY].iov_len = dnrs_len;
                        break;
+
+               case DHCPV4_OPT_CAPTIVE_PORTAL:
+                       size_t uri_len = iface->captive_portal_uri_len;
+                       if (uri_len == 0 || uri_len > UINT8_MAX)
+                               break;
+
+                       uint8_t *buf = alloca(2 + uri_len);
+                       struct dhcpv4_option *opt = (struct dhcpv4_option *)buf;
+
+                       opt->code = DHCPV4_OPT_CAPTIVE_PORTAL;
+                       opt->len  = uri_len;
+                       memcpy(opt->data, iface->captive_portal_uri, uri_len);
+
+                       iov[IOV_CAPTIVE_PORTAL].iov_base = opt;
+                       iov[IOV_CAPTIVE_PORTAL].iov_len  = 2 + uri_len;
+                       break;
                }
        }
 
index d2b31a3280046339638837e98723e3bba3f34745..f428022b3ffd1ff304e37b80c30ef871856ef7e1 100644 (file)
@@ -78,6 +78,7 @@ enum dhcpv4_opt {
        DHCPV4_OPT_CLIENTID = 61,
        DHCPV4_OPT_USER_CLASS = 77,
        DHCPV4_OPT_AUTHENTICATION = 90,
+       DHCPV4_OPT_CAPTIVE_PORTAL = 114, // RFC8910
        DHCPV4_OPT_SEARCH_DOMAIN = 119,
        DHCPV4_OPT_FORCERENEW_NONCE_CAPABLE = 145,
        DHCPV4_OPT_DNR = 162,